/**
 * LocationAPITools library, version 1.0.2
 * 
 * WWW: http://code.google.com/p/locationapitools
 *   
 * Summary:
 *   A collection of classes that simplify the development of location-aware mobile
 *   applications using the JSR-179 Location API on the J2ME (CLDC/MIDP) platform,
 *   e.g. on GPS-equipped smartphones.
 * 
 * Author:
 *   Matthias Stevens (Sony CSL Paris / Vrije Universiteit Brussel)
 *   Contact: matthias.stevens@gmail.com
 *  
 * Description:
 *   The primary use case is a location tracker that stores tracks in KML
 *   (cfr. Google Earth/Maps), CSV or plain text format.
 * 
 *   Package "com.sony.csl.j2me.locationapitools.demo" contains a MIDlet class that
 *   demonstrates how LocationAPITools can be used to create simple GPS tracker
 *   application that produces KML files. For examples of more advanced uses of this
 *   library refer to the NoiseTube project (see below, look for "NoiseTube MobileNoiseTagger").
 * 
 *   The code is known to be working on the following Symbian OS-based platforms:
 *     * S60 3rd Edition Feature Pack 1 (Symbian OS 9.2), e.g.: Nokia N95 and Nokia N95 8GB smartphones
 *     * S60 3rd Edition Feature Pack 2 (Symbian OS 9.3) 
 *   Although this is untested, the code should also work on other (non-Symbian/S60)
 *   J2ME CLDC/MIDP platforms/devices, as long as the JSR-179 Location API is supported
 *   on the platform/device in question.
 * 
 *   This library was developed within the scope of the NoiseTube project at
 *   Sony Computer Science Laboratory (CSL) Paris, for more information please refer to:
 *     * http://noisetube.net
 *     * http://code.google.com/p/noisetube
 *     * http://www.csl.sony.fr 
 *    
 * License: 
 *   Copyright 2008-2009 Sony CSL Paris
 *   Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 *   this file except in compliance with the License. You may obtain a copy of the
 *   License at http://www.apache.org/licenses/LICENSE-2.0.
 *   Unless required by applicable law or agreed to in writing, software distributed
 *   under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 *   CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *   specific language governing permissions and limitations under the License.
 */

package com.sony.csl.j2me.locationapitools;

import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.location.Location;
import javax.microedition.location.Coordinates;
import javax.microedition.location.LocationProvider;

/**
 * @author mstevens
 *
 */
public class CoordinatesCache implements LocationProxyListener
{
	
	private Vector cache;
	private long lastLocationTimeStamp;
	private int retentionTimeMilliSeconds;

	/**
	 * 
	 */
	public CoordinatesCache(LocationProxy locationProxy, int retentionTimeSeconds) 
	{
		retentionTimeMilliSeconds = retentionTimeSeconds * 1000;
		cache = new Vector();
		locationProxy.addListener(this); //!!!
	}

	public void firstLocationFound(LocationProxy proxy, LocationProvider provider, Location location) { }

	public void locationProviderStateChanged(LocationProxy proxy, LocationProvider provider, int newState) { }

	public void locationUpdated(LocationProxy proxy, LocationProvider provider, Location location)
	{
		if(location != null && location.isValid()) //just to be sure
		{
			cache.addElement(new CacheEntry(location.getTimestamp(), location.getQualifiedCoordinates()));
			lastLocationTimeStamp = location.getTimestamp();
			purgeOldCoordinates();
		}
	}
	
	private void purgeOldCoordinates()
	{
		if(!cache.isEmpty())
		{
			Vector cacheEntriesToRemove = new Vector();
			for(int e = 0; e < cache.size(); e++)
			{
				CacheEntry oldestCacheEntry = (CacheEntry) cache.elementAt(e);
				if(oldestCacheEntry.isOlderThan(lastLocationTimeStamp - retentionTimeMilliSeconds))
					cacheEntriesToRemove.addElement(oldestCacheEntry);
				else
					break;
			}
			Enumeration enumcacheEntriesToRemove = cacheEntriesToRemove.elements();
			while(enumcacheEntriesToRemove.hasMoreElements())
				cache.removeElement((CacheEntry) enumcacheEntriesToRemove.nextElement());
		}
	}

	public void proxyConnected(LocationProxy proxy, LocationProvider provider) { }

	public void proxyDisconnected(LocationProxy proxy) { }

	public boolean receivesInvalidLocations()
	{
		return false;
	}
	
	public Vector getCache()
	{
		return cache;
	}
	
	public boolean isEmpty()
	{
		return cache.isEmpty();
	}
	
	public int size()
	{
		return cache.size();
	}
	
	public Coordinates getOldestCoordinates()
	{
		if(!cache.isEmpty())
			return ((CacheEntry) cache.elementAt(0)).getCoordinates();
		else
			return null;
	}
	
	public Coordinates getNewestCoordinates()
	{
		if(!cache.isEmpty())
			return ((CacheEntry) cache.elementAt(cache.size() - 1)).getCoordinates();
		else
			return null;
	}
	
	public Coordinates getCoordinatesJustBefore(long timeStamp)
	{
		if(!cache.isEmpty())
		{
			if(timeStamp <= (lastLocationTimeStamp - retentionTimeMilliSeconds))
				return null;
			for(int e = cache.size() - 1; e >= 0; e--)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(cacheEntry.isOlderThan(timeStamp))
					return cacheEntry.getCoordinates();
			}
			return null;
		}
		else
			return null;		
	}
	
	public Coordinates getCoordinatesJustBeforeOrAt(long timeStamp)
	{
		if(!cache.isEmpty())
		{
			if(timeStamp < (lastLocationTimeStamp - retentionTimeMilliSeconds))
				return null;
			for(int e = cache.size() - 1; e >= 0; e--)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(cacheEntry.isOlderThanOrEquallyOldAs(timeStamp))
					return cacheEntry.getCoordinates();
			}
			return null;
		}
		else
			return null;	
	}
	
	public Coordinates getCoordinatesJustAfter(long timeStamp)
	{
		if(!cache.isEmpty())
		{
			if(timeStamp >= lastLocationTimeStamp)
				return null;
			for(int e = 0; e < cache.size(); e++)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(cacheEntry.isNewerThan(timeStamp))
					return cacheEntry.getCoordinates();
			}
			return null;
		}
		else
			return null;		
	}
	
	public Coordinates getCoordinatesJustAfterOrAt(long timeStamp)
	{
		if(!cache.isEmpty())
		{
			if(timeStamp > lastLocationTimeStamp)
				return null;
			for(int e = 0; e < cache.size(); e++)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(cacheEntry.isNewerThanOrEquallyOldAs(timeStamp))
					return cacheEntry.getCoordinates();
			}
			return null;
		}
		else
			return null;		
	}
	
	public Coordinates getCoordinatesClosestTo(long timeStamp)
	{
		if(!cache.isEmpty())
		{
			CacheEntry closestCacheEntry = null;			
			for(int e = 0; e < cache.size(); e++)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(closestCacheEntry == null)
					closestCacheEntry = cacheEntry;
				else
				{
					if(Math.abs(timeStamp - cacheEntry.getTimeStamp()) < Math.abs(timeStamp - closestCacheEntry.getTimeStamp()))
						closestCacheEntry = cacheEntry;
				}
			}
			return closestCacheEntry.getCoordinates();
		}
		else
			return null;
	}
	
	public Coordinates[] getAllCoordinatesBetween(long begin, long end)
	{
		return getAllCoordinatesBetween(begin, end, true);
	}
	
	public Coordinates[] getAllCoordinatesBetween(long begin, long end, boolean inclusive)
	{
		if(!cache.isEmpty())
		{
			Vector allCoordinatesInBetween = new Vector();
			for(int e = 0; e < cache.size(); e++)
			{
				CacheEntry cacheEntry = (CacheEntry) cache.elementAt(e);
				if(inclusive)
				{
					if(cacheEntry.isNewerThanOrEquallyOldAs(begin) && cacheEntry.isOlderThanOrEquallyOldAs(end))
						allCoordinatesInBetween.addElement(cacheEntry.getCoordinates());
				}
				else
				{
					if(cacheEntry.isNewerThan(begin) && cacheEntry.isOlderThan(end))
						allCoordinatesInBetween.addElement(cacheEntry.getCoordinates());
				}
			}
			if(!allCoordinatesInBetween.isEmpty())
			{
				Coordinates[] allCoordinatesInBetween_return = new Coordinates[allCoordinatesInBetween.size()];
				allCoordinatesInBetween.copyInto(allCoordinatesInBetween_return);
				return allCoordinatesInBetween_return;
			}
			else
				return null;
		}
		else
			return null;	
	}
	
	private class CacheEntry
	{
	
		private long timeStamp;
		private Coordinates coordinates;
		
		public CacheEntry(long timeStamp, Coordinates coordinates)
		{
			this.timeStamp = timeStamp;
			this.coordinates = coordinates;
		}
		
		public long getTimeStamp()
		{
			return timeStamp;
		}
		
		public Coordinates getCoordinates()
		{
			return coordinates;
		}
		
		public boolean isOlderThan(long timeStamp)
		{
			return (this.timeStamp < timeStamp);
		}
		
		public boolean isOlderThanOrEquallyOldAs(long timeStamp)
		{
			return (this.timeStamp <= timeStamp);
		}
		
		public boolean isNewerThan(long timeStamp)
		{
			return (this.timeStamp > timeStamp);
		}
		
		public boolean isNewerThanOrEquallyOldAs(long timeStamp)
		{
			return (this.timeStamp >= timeStamp);
		}
		
	}

}
